6.8 Polymorphie
 
Ruft ein Client eine Methode in einem Subklassenobjekt auf, wird in der Subklasse geprüft, ob sie eine Methode mit diesem Namen und exakt denselben Parametern enthält. Handelt es sich tatsächlich um eine Subklassenmethode, wird sie ausgeführt, andernfalls wird der Aufruf an die direkte Basisklasse weitergeleitet, in der nach denselben Kriterien gesucht wird.
Gegebenenfalls durchläuft der Mechanismus die gesamte Vererbungslinie bis hin zur .NET-Basisklasse Object. Wird in der gesamten Vererbungskette keine passende Methode gefunden, kommt es zu einem Laufzeitfehler. Befinden sich zwei oder mehr gleichnamige Methoden mit identischer Signatur in der Vererbungskette, wird die Methode ausgeführt, die zuerst die Bedingungen erfüllt. Dazu ein Beispiel:
| class ClassA {
|
| public void MyMethod() {
|
| Console.WriteLine("MyMethod von ClassA");
|
| }
|
| }
|
| class ClassB : ClassA {
|
| public new void MyMethod() {
|
| Console.WriteLine("MyMethod von ClassB");
|
| }
|
| }
|
Mit
| ClassB obj = new ClassB();
|
| obj.MyMethod();
|
wird die in der Subklasse definierte Methode ausgeführt und im Befehlsfenster
angezeigt.
Schlagen wir jetzt einen anderen Weg ein und instanziieren ein Objekt der abgeleiteten Klasse auf folgende Weise:
| ClassA obj = new ClassB();
|
Im Vergleich zu der vorhergehenden Instanziierung ist nun eine Variable vom Typ der Basisklasse deklariert, der wir die Referenz auf ein Objekt der abgeleiteten Klasse zuweisen. Weil ein ClassB-Objekt gleichzeitig auch ein Objekt seiner Basisklasse ist, ist diese Zuweisung nicht zu beanstanden. Rufen wir jetzt erneut die Methode MyMethod auf, erhalten wir ein völlig anderes Ergebnis im Konsolenfenster. Es wird nicht mehr die Methode der abgeleiteten Klasse, sondern die der Basisklasse ausgeführt:
Das Ergebnis ist zwar nicht spektakulär, hat aber weit reichende Konsequenzen. Wir müssen uns nämlich die Frage stellen, ob die Ausgabe das ist, was wir erreichen wollten. Vermutlich nicht, denn eigentlich sollte doch die klassenspezifische Methode in ClassB, der abgeleiteten Klasse, ausgeführt werden.
Das Beispiel macht deutlich, welchen Nebeneffekt das Überdecken einer Methode der Basisklasse haben kann: Der Compiler betrachtet das Objekt, als wäre es vom Typ der Basisklasse, und ruft die unter Umständen aus logischer Sicht sogar fehlerhafte Methode in der Basisklasse auf.
Ursächliches Problem ist das statische Binden des Methodenaufrufs an die Basisklasse. Statisches Binden heißt, dass die auszuführende Operation bereits zur Kompilierzeit festgelegt wird. Der Compiler stellt fest, von welchem Typ das Objekt ist, auf das die Methode aufgerufen wird, und erzeugt den entsprechenden Code. Das statische Binden führt dazu, dass die verdeckte Methode der Basisklasse aufgerufen wird, obwohl eigentlich die neue Methode in der abgeleiteten Klasse erforderlich wäre.
6.8.1 Virtuelle Methoden
 
Um auf die Referenz eines Basisklassentyps die Methode in der abgeleiteten Klasse aufrufen zu können, muss die statische Bindung aufgehoben werden. Dazu dienen die beiden Schlüsselwörter virtual und override. Definieren Sie in der Basisklasse eine Methode virtual, kann sie von einer Subklassenmethode mit override überschrieben werden. Der Compiler bindet dann nicht mehr statisch zur Kompilierzeit, sondern dynamisch zur Laufzeit.
Ergänzen Sie die Definition der Methode MyMethod in der Basisklasse um virtual:
| // die Basisklasse
|
| class ClassA {
|
| public virtual void MyMethod() {
|
| Console.WriteLine("MyMethod von ClassA");
|
| }
|
| }
|
Eine ableitende Klasse, die eine virtuelle Methode der Basisklasse durch eine eigene Implementierung überschreiben möchte, kann die Methodendefinition um den Modifizierer override erweitern:
| // die abgeleitete Klasse
|
| class ClassB : ClassA {
|
| public override void MyMethod() {
|
| Console.WriteLine("MyMethod von ClassB");
|
| }
|
| }
|
Wird nun mit
| ClassA obj = new ClassB();
|
| obj.MyMethod();
|
derselbe Testcode wie oben ausgeführt, ist das Ergebnis ein völlig anderes: Erst wenn es zur Laufzeit zum Aufruf der Methode MyMethod kommt, ermittelt die Laufzeitumgebung den tatsächlichen Typ der Referenz obj und stellt fest, dass es sich um ClassB handelt. Deren Methode wird aufgerufen und gibt im Konsolenfenster
aus. Anscheinend ist das Objekt obj in der Lage zu entscheiden, welche Methode auf sich selbst anzuwenden ist – unabhängig davon, wo es in der Vererbungslinie steht. Diese Fähigkeit wird als Polymorphie bezeichnet und ist neben der Kapselung und der Vererbung die dritte Stütze der objektorientierten Programmierung.
| Polymorphie bezeichnet ein Konzept der Objektorientierung, das besagt, dass Objekte bei gleichen Methodenaufrufen unterschiedlich reagieren können. Objekte verschiedener Typen können dabei unter einem gemeinsamen Oberbegriff betrachtet werden. Die Polymorphie sorgt dafür, dass der Methodenaufruf automatisch bei der richtigen Methode landet – nämlich der, die in der Klasse des Objekts implementiert ist.
|
Polymorphie arbeitet mit dynamischer Bindung. Der Aufrufcode wird nicht zur Kompilierzeit erzeugt, sondern erst zur Laufzeit der Anwendung, wenn die konkreten Typinformationen vorliegen, um die tatsächlich aufzurufende Methode zu bestimmen. Im Gegensatz dazu legt die statische Bindung die auszuführende Operation bereits zur Kompilierzeit fest.
Beachten Sie, dass eine statische Methode nicht virtuell sein kann. Ebenso ist eine Kombination des Schlüsselworts virtual mit abstract oder override nicht zulässig. Hinter der Definition einer virtuellen Methode verbirgt sich die Absicht, polymorphes Verhalten zu ermöglichen. Daher macht es auch keinen Sinn, ein privates Klassenmitglied virtual zu deklarieren – es kommt zu einem Kompilierfehler. new und override dürfen nicht für dasselbe Member verwendet werden, sie schließen sich gegenseitig aus.
6.8.2 Inhomogene Mengen
 
In inhomogenen Mengen, die sich in einer Basisklasse unter einem gemeinsamen Oberbegriff zusammenfassen lassen, spielt die Polymorphie eine wichtige Rolle, um die jeweils richtige Methode aufzurufen. Inhomogene Mengen werden durch Arrays vom Typ einer Basisklasse abgebildet, denen Objekte derselben oder einer abgeleiteten Klasse zugewiesen werden.
Betrachten wir die Klassenhierarchie der Luftfahrzeuge und hier insbesondere der Methode Starten der Klasse Luftfahrzeug. Im Abschnitt 6.7 haben Sie gesehen, dass es sich anbietet, diese Methode abstrakt zu definieren. Im folgenden Beispiel wird Starten in der Klasse Luftfahrzeug implementiert, allerdings virtuell.
| // --------------------------------------------------------------
|
| // Beispiel: ...\Kapitel 6\Polymorphie
|
| // --------------------------------------------------------------
|
| class Luftfahrzeug {
|
| public virtual void Starten() {
|
| Console.WriteLine("Das Luftfahrzeug startet");
|
| }
|
| }
|
| class Hubschrauber : Luftfahrzeug {
|
| public override void Starten() {
|
| Console.WriteLine("Der Hubschrauber startet");
|
| }
|
| }
|
| class Zeppelin : Luftfahrzeug {
|
| public override void Starten() {
|
| Console.WriteLine("Der Zeppelin startet");
|
| }
|
| }
|
| class Flugzeug : Luftfahrzeug { }
|
Zeppelin, Hubschrauber und Flugzeug leiten die Klasse Luftfahrzeug ab. Während für ein herkömmliches Flugzeug die Implementierung der Methode in Luftfahrzeug ausreichend ist, überschreiben die beiden anderen Subklassen mit override die geerbte Methode Starten.
Müssen mehrere verschiedene Typen aus einer Vererbungslinie gleichzeitig verwaltet werden, bietet es sich an, dafür ein Array vorzusehen. Sinnvollerweise wird das Array von dem Typ deklariert, welcher der allgemeinste aller verwalteten Objekte ist. In unserem Fall handelt es sich demnach um Luftfahrzeug:
| Luftfahrzeug[] lfzg = new Luftfahrzeug[11];
|
Das Array lfzg kann maximal elf Objekte vom Typ Luftfahrzeug aufnehmen, einschließlich der abgeleiteten Typen. In beliebiger Reihenfolge lassen sich den Array-Elementen Referenzen auf die Typen zuweisen. Program enthält eine statische Methode namens MakeType, in der die Bestimmung des Typs dem Zufallszahlengenerator Random überlassen wird.
| class Program {
|
| static void Main(string[] args) {
|
| Luftfahrzeug[] lfzg = MakeType();
|
| // Inhalt des Arrays lfzg ausgeben
|
| for(int i = 0; i <= 10; i++)
|
| Console.Write("Index = {0} / Typ: {1}\n", i, lfzg[i].ToString());
|
| Console.WriteLine("----------------------------------");
|
| // alle Hubschrauber starten
|
| for(int i = 0; i <= 10; i++) {
|
| if(lfzg[i] is Hubschrauber) {
|
| Console.Write("Index = {0} / ", i);
|
| lfzg[i].Starten();
|
| }
|
| }
|
| Console.ReadLine();
|
| }
|
| // ein Array mit Objekten vom Typ Luftfahrzeug füllen
|
| static Luftfahrzeug[] MakeType() {
|
| Luftfahrzeug[] arrLfzg = new Luftfahrzeug[11];
|
| Random rnd = new Random();
|
| int type;
|
| for(int i = 0; i <= 10; i++) {
|
| // Zufallszahl im Bereich 0 <= type < 3
|
| type = rnd.Next(3);
|
| switch(type) {
|
| case 0:
|
| arrLfzg[i] = new Zeppelin();
|
| break;
|
| case 1:
|
| arrLfzg[i] = new Hubschrauber();
|
| break;
|
| case 2:
|
| arrLfzg[i] = new Flugzeug();
|
| break;
|
| }
|
| }
|
| // Referenz des Arrays an den Aufrufer liefern
|
| return arrLfzg;
|
| }
|
| }
|
In Main wird der Rückgabewert von MakeType direkt einem Array zugewiesen:
| Luftfahrzeug[] lfzg = MakeType();
|
Anschließend werden die Elemente inklusive der Angabe der den Indizes zugeordneten Typen an der Konsole ausgegeben. Um das polymorphe Verhalten zu demonstrieren, werden exemplarisch alle Hubschrauber gestartet. Dazu muss zuerst der Typ, der sich hinter jedem Array-Element verbirgt, mit
| if(lfzg[i] is Hubschrauber)...
|
ermittelt werden. Liefert die Typbestimmung true, wird mit
der Hubschrauber gestartet.
Durch die Definition virtual in der Basisklasse gestehen wir der Methode polymorphes Verhalten zu. Unabhängig davon, ob sich hinter einem Array-Element, das vom Typ der Basisklasse Luftfahrzeug ist, tatsächlich ein Flugzeug, Hubschrauber oder Zeppelin verbirgt, wird der Aufruf immer polymorph an die »richtige« Methode weitergeleitet.
6.8.3 Verdecken und Überschreiben geerbter Methoden
 
Um polymorphes Verhalten zu ermöglichen, muss eine Methode virtual definiert sein. In einer abgeleiteten Klasse kann dieses Angebot angenommen oder abgelehnt werden.
|
Implementieren Sie in der abgeleiteten Klasse die geerbte Methode mit dem Schlüsselwort override, wird die ursprüngliche Methode überschrieben – die abgeleitete Klasse akzeptiert das Angebot der Basisklasse. Ein Aufruf an eine Referenz des Oberbegriffs wird polymorph an den sich tatsächlich dahinter verbergenden Typ weitergeleitet. |
|
In der abgeleiteten Klasse kann eine virtuelle Methode auch mit dem Modifizierer new ausgeblendet werden. Dann verdeckt die geerbte Methode in der Subklasse die Implementierung in der Basisklasse und zeigt kein polymorphes Verhalten mehr. |
Es ist sogar möglich, innerhalb einer Vererbungskette ein gemischtes Verhalten von Ausblendung und Überschreibung vorzusehen, wie das folgende Codefragment zeigt:
| class ClassA {
|
| public virtual void TestMethod() { }
|
| }
|
| class ClassB : ClassA {
|
| public override void TestMethod () { }
|
| }
|
| class ClassC : ClassB {
|
| public new void TestMethod () { }
|
| }
|
ClassA bietet die virtuelle Methode TestMethod an. ClassB als Subklasse von ClassA überschreibt mit override, die zweite Ableitung in der ClassC verdeckt jedoch nur noch mit new.
Wenn Sie nun nach der Zuweisung
| ClassA obj = new ClassC();
|
auf die Referenz obj die Methode TestMethod aufrufen, wird die Methode TestMethod in ClassB ausgeführt, da diese die aus ClassA geerbte polymorph überschreibt. TestMethod zeigt aber in der Klasse ClassC wegen des Modifizierers new kein polymorphes Verhalten mehr.
Das Überschreiben einer mit new überdeckenden Methode mit override ist nicht möglich:
| class ClassB : ClassA {
|
| public new void TestMethod() { }
|
| }
|
| class ClassC : ClassB {
|
| public override void TestMethod () { }
|
| }
|
Der C#-Compiler wird nun eine Fehlermeldung ausgeben.
6.8.4 Überschreiben der Methode »ToString()« der Klasse »Object«
 
Die Klasse Object, aus der jede Klasse des .NET Frameworks abgeleitet wird, vererbt jeder Klasse eine Reihe elementarer Methoden. Eine davon, ToString, haben wir in unserem Beispiel Polymorphie dazu benutzt, um im Befehlsfenster den Typ, der sich hinter einem Array-Element verbirgt, ausgeben zu lassen.
Vielleicht gefällt uns der Rückgabewert der Standardimplementierung von ToString nicht, der immer den voll qualifizierenden Namen liefert. ToString ist ein typischer Kandidat zum polymorphen Überschreiben, um eine typspezifisch angepasste Zeichenfolge zu erhalten. In der Praxis wird das tatsächlich auch häufig gemacht. In der .NET-Dokumentation ist die Definition dieser Methode wie folgt angegeben:
| public virtual string ToString();
|
Überschrieben werden sollte ToString in jeder Klasse mit override, um das polymorphe Verhalten durchzusetzen. Stellvertretend für die Klassen Flugzeug, Zeppelin und Hubschrauber sei an dieser Stelle nur die Ergänzung der Klasse Hubschrauber gezeigt:
| class Hubschrauber : Luftfahrzeug {
|
| override public void Starten() {
|
| Console.WriteLine("Der Hubschrauber startet");
|
| }
|
| public override string ToString() {
|
| return "Hubschrauber";
|
| }
|
| }
|
Geerbte virtuelle Methoden
Jede Methode einer Basisklasse wird an die abgeleitete Klasse vererbt. Ist die Methode darüber hinaus virtuell definiert, sollten Sie das als ein Angebot der Basisklasse verstehen, das Sie annehmen oder ablehnen können. Nehmen Sie das Angebot wahr, überschreiben Sie die geerbte Methode mit override und setzen damit polymorphes Verhalten durch. Sie können das Angebot auch ablehnen und dennoch die geerbte Methode in der abgeleiteten Klasse neu implementieren. Dazu gibt es den Modifizierer new, der aber bekanntermaßen nicht nur auf geerbte, virtuelle Methoden eingesetzt werden kann. Es gibt nur relativ wenig Methoden, die das Angebot virtueller Methoden auf diese Weise ausschlagen.
Ob Sie eine Methode in einer ableitbaren Klasse virtuell zur Verfügung stellen oder wie Sie auf das Angebot in der Subklasse reagieren, kann im Einzelfall unterschiedlich sein und muss eingehend geprüft werden.
6.8.5 Versiegelte Methoden
 
Standardmäßig können Klassen abgeleitet werden. Ist dieses Verhalten für eine bestimmte Klasse nicht gewünscht, kann die Klasse mit sealed versiegelt werden und ist dann nicht ableitbar.
In ähnlicher Weise können Sie dem weiteren Überschreiben einer Methode auch einen Riegel vorschieben, indem die Definition der Methode um den Modifizierer sealed ergänzt wird:
| class ClassB : ClassA {
|
| public sealed override void TestMethod() {
|
| Console.WriteLine("TestProc in ClassB");
|
| }
|
| }
|
Eine von ClassB abgeleitete Klasse erbt zwar die versiegelte Methode, kann sie aber selbst nicht mit override überschreiben. Es ist jedoch möglich, in einer weiter abgeleiteten Klasse eine geerbte, versiegelte Methode mit new zu überdecken, um eine typspezifische Anpassung vornehmen zu können.
Der Modifizierer sealed kann nur zusammen mit override in einer Methodensignatur einer abgeleiteten Klasse verwendet werden, wenn die Methode in der Basisklasse als virtuelle Methode bereitgestellt wird. Die Kombination sealed new ist unzulässig, ebenso das alleinige Verwenden von sealed.
6.8.6 Zusammenfassung
 
|
Methoden ohne Anweisungsblock legen kein Objektverhalten fest und werden als abstrakte Methoden bezeichnet. Abstrakte Methoden werden mit dem Modifizierer abstract gekennzeichnet und hinter der Parameterliste mit einem Semikolon abgeschlossen. Statische Methoden können niemals abstrakt sein. |
|
Klassen, die mindestens eine abstrakte Methode haben, sind selbst abstrakt und müssen mit dem Modifizierer abstract signiert werden. Eine Klasse kann auch dann abstrakt sein, wenn keines ihrer Mitglieder abstrakt ist. |
|
Abstrakte Klassen können nicht instanziiert werden. Deshalb machen abstrakte Klassendefinitionen nur dann Sinn, wenn sie abgeleitet werden. |
|
Eine nichtabstrakte Klasse, die von einer abstrakten Klasse abgeleitet wird, muss alle geerbten abstrakten Methoden implementieren. |
|
Eine geerbte abstrakte Methode wird in der abgeleiteten Klasse durch eine Signatur, die den override-Modifizierer verwendet, überschrieben. Wird die geerbte abstrakte Methode nicht überschrieben, gilt auch die abgeleitete Klasse als abstrakt. Überschriebene abstrakte Methoden zeigen immer polymorhes Verhalten. |
|
Polymorphie ist einer der Stützpfeiler der objektorientierten Programmierung. Polymorphie bedeutet, dass Objekte bei gleichen Methodenaufrufen unterschiedlich reagieren können, wenn die Objekte unter einem gemeinsamen Oberbegriff betrachtet werden. |
|
Eine Methode, die sich polymorph verhalten soll, muss entweder abstract oder virtual deklariert sein. Polymorphe Methodenaufrufe werden dynamisch zur Laufzeit gebunden, wenn in der abgeleiteten Klasse die virtuelle Methode mit override überschrieben wird. Wird eine virtuelle Methode in der Subklasse mit new ausgeblendet, setzt sich das polymorphe Verhalten nicht durch, und der Methodenaufruf wird statisch gebunden. |
|
Die Modifiziererkombination sealed override einer Methode verhindert, dass sie nicht polymorph, also mit override, in einer abgeleiteten Klasse überschrieben werden kann. |
|